import React, { useState, useEffect, useMemo } from 'react'; import { Wrench, Settings, User, TrendingUp, Plus, PlusCircle, ClipboardCheck, Truck, Shield, Activity, FileText, AlertTriangle, Trash2, Edit, Search, Download, RefreshCw, CheckCircle, XCircle, Clock, Eye, ChevronRight, Package, Disc, Navigation, Check, BarChart2, ShieldAlert, Calendar, Users, UserCheck, FileSpreadsheet, Layers, ExternalLink, LogOut, Camera, Upload, Info, MapPin, Maximize2, AlertCircle } from 'lucide-react'; const MECHANICS_LIST = [ "Alvi", "Wahiyo", "Topik", "Chandra", "Apri", "Obi", "Dwi", "Khabul", "Dian", "Ega", "Gagah", "Rio", "Agung", "Agus", "Eko", "Deril", "Wisnu", "Tegar", "Anja", "Wahyu", "Anas", "Rabani" ]; const CHECKERS_LIST = [ "Wahyu", "Bono", "Andi", "Firdaus" ]; // 1. GENERATE 100 UNIT BUS TSW (TSW 001 - TSW 100) OTOMATIS const FLEET_UNITS = Array.from({length: 100}, (_, i) => `TSW ${String(i + 1).padStart(3, '0')}`); const DEFAULT_SPAREPARTS = [ { kode: "SP-BRK-01", nama: "Kampas Rem Depan Heavy Duty (Set)", qty: 24, unit: "Set", minQty: 5 }, { kode: "SP-OIL-15W", nama: "Oli Mesin Synthetic 15W-40", qty: 210, unit: "Liter", minQty: 40 }, { kode: "SP-FIL-O1", nama: "Filter Oli Bus Scania Type-A", qty: 15, unit: "Pcs", minQty: 4 }, { kode: "SP-BELT-99", nama: "Fan Belt High-Tension v3", qty: 12, unit: "Pcs", minQty: 3 }, { kode: "SP-TIRE-295", nama: "Ban Radial Tubeless 295/80 R22.5", qty: 18, unit: "Pcs", minQty: 4 }, { kode: "SP-ALT-24V", nama: "Alternator Heavy Duty 24V 150A", qty: 3, unit: "Pcs", minQty: 2 }, { kode: "SP-CLU-M1", nama: "Clutch Plate Master Kit Scania", qty: 4, unit: "Set", minQty: 2 } ]; const DEFAULT_VEHICLES_PM = [ { noBody: "TSW 001", currentKM: 45200, nextPMTarget: 50000, model: "Scania K360IB", status: "Aman" }, { noBody: "TSW 045", currentKM: 119850, nextPMTarget: 120000, model: "Mercedes-Benz OH 1626", status: "Mendekati PM" }, { noBody: "TSW 022", currentKM: 92120, nextPMTarget: 90000, model: "Hino RK8", status: "Overdue" }, { noBody: "TSW 089", currentKM: 201300, nextPMTarget: 210000, model: "Volvo B11R", status: "Aman" }, { noBody: "TSW 100", currentKM: 15150, nextPMTarget: 20000, model: "Mercedes-Benz OF 917", status: "Aman" } ]; const DEFAULT_WORK_ORDERS = [ { id: "WO-9901", noBody: "TSW 001", kmMasuk: 45200, keluhan: "AC kabin belakang berisik & embusan udara kurang dingin mendadak", prioritas: "Sedang", status: "Pending", mekanik: [], jamMulai: "", jamSelesai: "", durasi: 0, tindakan: "", sparepart: [], checker: "Bono", tanggal: "2026-05-20", fotoBefore: "https://images.unsplash.com/photo-1544620347-c4fd4a3d5957?w=600&auto=format&fit=crop&q=60", fotoAfter: "" }, { id: "WO-9902", noBody: "TSW 045", kmMasuk: 119850, keluhan: "Pedal kopling keras & ada rembesan oli tipis di dekat transmisi bawah", prioritas: "Tinggi", status: "Aktif", mekanik: ["Alvi", "Wahiyo"], jamMulai: "2026-05-22T08:00", jamSelesai: "", durasi: 0, tindakan: "", sparepart: [], checker: "Andi", tanggal: "2026-05-22", fotoBefore: "https://images.unsplash.com/photo-1517524206127-48bbd363f3d7?w=600&auto=format&fit=crop&q=60", fotoAfter: "" } ]; const DEFAULT_STORING = [ { id: "STR-5001", noBody: "TSW 089", lokasi: "Halte Busway Pancoran Barat Arah Grogol", kendala: "Pipa radiator pecah menyebabkan air pendingin habis & mesin overheat", tindakan: "Potong bagian pipa bocor, pasang clamp bypass darurat, isi air coolant penuh", mekanik: ["Obi", "Khabul"], jamMulai: "2026-05-22T14:00", jamSelesai: "2026-05-22T15:30", status: "Selesai", foto: "https://images.unsplash.com/photo-1511919884226-fd3cad34687c?w=600&auto=format&fit=crop&q=60" }, { id: "STR-5002", noBody: "TSW 100", lokasi: "Samping Gerbang Tol Kebon Jeruk 1", kendala: "Angin kompresor drop tiba-tiba, rem mengunci otomatis di lajur kiri", tindakan: "", mekanik: ["Alvi", "Topik"], jamMulai: "2026-05-24T09:00", jamSelesai: "", status: "OTW", foto: "" } ]; const DEFAULT_TIRE_SWAPS = [ { id: "BAN-8001", noBody: "TSW 001", posisi: "Roda Depan Kanan", kmBan: 85200, barcodeLama: "BC-SL-8812A", barcodeBaru: "BC-SN-9905X", mekanik: ["Wahiyo", "Apri"], jamMulai: "2026-05-18T10:00", jamSelesai: "2026-05-18T11:15", fotoLama: "https://images.unsplash.com/photo-1578844251758-2f71da64c96f?w=400", fotoBaru: "https://images.unsplash.com/photo-1580273916550-e323be2ae537?w=400" } ]; export default function App() { const [isLoggedIn, setIsLoggedIn] = useState(() => { return localStorage.getItem('tj_logged_in') === 'true'; }); const [currentRole, setCurrentRole] = useState(() => { return localStorage.getItem('tj_workshop_role') || 'checker'; }); const [currentUserName, setCurrentUserName] = useState(() => { return localStorage.getItem('tj_user_name') || ''; }); const [workOrders, setWorkOrders] = useState(() => { const saved = localStorage.getItem('tj_db_work_orders'); return saved ? JSON.parse(saved) : DEFAULT_WORK_ORDERS; }); const [spareparts, setSpareparts] = useState(() => { const saved = localStorage.getItem('tj_db_spareparts'); return saved ? JSON.parse(saved) : DEFAULT_SPAREPARTS; }); const [vehiclesPM, setVehiclesPM] = useState(() => { const saved = localStorage.getItem('tj_db_vehicles_pm'); return saved ? JSON.parse(saved) : DEFAULT_VEHICLES_PM; }); const [storingList, setStoringList] = useState(() => { const saved = localStorage.getItem('tj_db_storing_list'); return saved ? JSON.parse(saved) : DEFAULT_STORING; }); const [tireSwaps, setTireSwaps] = useState(() => { const saved = localStorage.getItem('tj_db_tire_swaps'); return saved ? JSON.parse(saved) : DEFAULT_TIRE_SWAPS; }); const [inspectionLogs, setInspectionLogs] = useState(() => { const saved = localStorage.getItem('tj_db_inspection_logs'); return saved ? JSON.parse(saved) : []; }); const [spreadsheetUrl, setSpreadsheetUrl] = useState(() => { return localStorage.getItem('tj_sheet_webhook') || ''; }); const [activeTab, setActiveTab] = useState('dashboard'); const [mechanicSubTab, setMechanicSubTab] = useState('tugas'); const [toasts, setToasts] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [showConfirmReset, setShowConfirmReset] = useState(false); const [leaderAssignWO, setLeaderAssignWO] = useState(null); const [assignedMeks, setAssignedMeks] = useState([]); const [leaderNotes, setLeaderNotes] = useState(''); const [estimatedDuration, setEstimatedDuration] = useState('4'); const [tempBase64Image, setTempBase64Image] = useState(''); const [tempBase64ImageAfter, setTempBase64ImageAfter] = useState(''); const [newPartForm, setNewPartForm] = useState({ kode: '', nama: '', qty: '', unit: 'Pcs', minQty: '3' }); const [isEditingPart, setIsEditingPart] = useState(null); const [showAddPartModal, setShowAddPartModal] = useState(false); const [zoomImgUrl, setZoomImgUrl] = useState(null); useEffect(() => { localStorage.setItem('tj_logged_in', isLoggedIn); localStorage.setItem('tj_workshop_role', currentRole); localStorage.setItem('tj_user_name', currentUserName); }, [isLoggedIn, currentRole, currentUserName]); useEffect(() => { localStorage.setItem('tj_db_work_orders', JSON.stringify(workOrders)); }, [workOrders]); useEffect(() => { localStorage.setItem('tj_db_spareparts', JSON.stringify(spareparts)); }, [spareparts]); useEffect(() => { localStorage.setItem('tj_db_vehicles_pm', JSON.stringify(vehiclesPM)); }, [vehiclesPM]); useEffect(() => { localStorage.setItem('tj_db_storing_list', JSON.stringify(storingList)); }, [storingList]); useEffect(() => { localStorage.setItem('tj_db_tire_swaps', JSON.stringify(tireSwaps)); }, [tireSwaps]); useEffect(() => { localStorage.setItem('tj_db_inspection_logs', JSON.stringify(inspectionLogs)); }, [inspectionLogs]); useEffect(() => { localStorage.setItem('tj_sheet_webhook', spreadsheetUrl); }, [spreadsheetUrl]); const triggerToast = (msg, type = 'success') => { const id = Date.now(); setToasts(prev => [...prev, { id, msg, type }]); setTimeout(() => { setToasts(prev => prev.filter(t => t.id !== id)); }, 4000); }; const removeToast = (id) => { setToasts(prev => prev.filter(t => t.id !== id)); }; const sendToGoogleSheets = async (sheetType, payloadData) => { if (!spreadsheetUrl) return; try { const response = await fetch(spreadsheetUrl, { method: "POST", mode: "no-cors", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sheet: sheetType, data: payloadData }) }); triggerToast("📡 Sinkronisasi Google Sheets berhasil berjalan di latar belakang!", "success"); } catch (err) { console.warn("Spreadsheet dispatch failure", err); } }; const handleFileChange = (e, setTargetState) => { const file = e.target.files[0]; if (file) { if (file.size > 1.5 * 1024 * 1024) { triggerToast("Ukuran berkas terlalu besar! Maksimal 1.5MB.", "error"); return; } const reader = new FileReader(); reader.onloadend = () => { setTargetState(reader.result); triggerToast("Foto berhasil dimuat!", "success"); }; reader.readAsDataURL(file); } }; const handleLogin = (role, name) => { if (!name) { triggerToast("Pilih nama Anda sebelum masuk!", "error"); return; } setCurrentRole(role); setCurrentUserName(name); setIsLoggedIn(true); if (role === 'checker') setActiveTab('checker_form'); else if (role === 'mechanic') setActiveTab('mechanic_tasks'); else setActiveTab('dashboard'); triggerToast(`Selamat datang kembali, ${name}! (${role.toUpperCase()})`, "success"); }; const handleLogout = () => { setIsLoggedIn(false); setCurrentUserName(''); triggerToast("Anda berhasil keluar dari sistem.", "warning"); }; const stats = useMemo(() => { const total = workOrders.length; const pending = workOrders.filter(w => w.status === 'Pending').length; const aktif = workOrders.filter(w => w.status === 'Aktif').length; const waiting = workOrders.filter(w => w.status === 'Menunggu Approval').length; const approved = workOrders.filter(w => w.status === 'Approved').length; const rejected = workOrders.filter(w => w.status === 'Rejected').length; const performance = MECHANICS_LIST.map(name => { const assignedJobs = workOrders.filter(w => w.mekanik.includes(name)); const completedJobs = assignedJobs.filter(w => w.status === 'Approved'); let totalTime = 0; completedJobs.forEach(j => { if (j.durasi) totalTime += j.durasi; }); const avgDur = completedJobs.length > 0 ? Math.round(totalTime / completedJobs.length) : 0; const totalStoring = storingList.filter(s => s.mekanik.includes(name) && s.status === 'Selesai').length; const totalTire = tireSwaps.filter(t => t.mekanik.includes(name)).length; const score = (completedJobs.length * 15) + (totalStoring * 20) + (totalTire * 10); return { name, assignedCount: assignedJobs.length, completedCount: completedJobs.length, avgDur, storingCount: totalStoring, tireCount: totalTire, score }; }).sort((a, b) => b.score - a.score); const lowStockParts = spareparts.filter(s => s.qty <= s.minQty); const overduePM = vehiclesPM.filter(v => v.currentKM >= v.nextPMTarget).length; const nearPM = vehiclesPM.filter(v => v.currentKM < v.nextPMTarget && (v.nextPMTarget - v.currentKM) <= 2000).length; return { total, pending, aktif, waiting, approved, rejected, performance, lowStockCount: lowStockParts.length, lowStockParts, overduePM, nearPM }; }, [workOrders, spareparts, storingList, tireSwaps, vehiclesPM]); const handleCreateWO = (e) => { e.preventDefault(); const formData = new FormData(e.target); const noBody = formData.get('noBody').toUpperCase().trim(); const kmMasuk = parseInt(formData.get('kmMasuk')); const keluhan = (formData.get('keluhan') || '').trim(); const prioritas = formData.get('prioritas'); if (!noBody || !kmMasuk) { triggerToast("Harap isi No Body dan Odometer wajib!", "error"); return; } const hasFinding = keluhan.length > 0; const newLog = { id: `INSP-${Date.now().toString().slice(-4)}`, noBody, kmMasuk, keluhan: hasFinding ? keluhan : "Tidak ada keluhan (Kondisi Unit Normal / Prima)", prioritas: hasFinding ? prioritas : "Rendah", checker: currentUserName, tanggal: new Date().toISOString().split('T')[0], status: hasFinding ? "Temuan Kerusakan" : "Armada Prima / Siap Dinas", fotoBefore: tempBase64Image || "https://images.unsplash.com/photo-1544620347-c4fd4a3d5957?w=600&auto=format&fit=crop&q=60" }; setInspectionLogs(prev => [newLog, ...prev]); sendToGoogleSheets("inspeksi", newLog); setVehiclesPM(prev => { const exists = prev.find(v => v.noBody === noBody); if (exists) { return prev.map(v => v.noBody === noBody ? { ...v, currentKM: kmMasuk } : v); } else { return [...prev, { noBody, currentKM: kmMasuk, nextPMTarget: Math.ceil((kmMasuk + 1) / 10000) * 10000, model: "Bus Transjakarta Hino/Scania", status: "Aman" }]; } }); if (hasFinding) { const newWO = { id: `WO-${Date.now().toString().slice(-4)}`, noBody, kmMasuk, keluhan, prioritas, status: "Pending", mekanik: [], jamMulai: "", jamSelesai: "", durasi: 0, tindakan: "", sparepart: [], checker: currentUserName, tanggal: new Date().toISOString().split('T')[0], fotoBefore: tempBase64Image || "https://images.unsplash.com/photo-1544620347-c4fd4a3d5957?w=600&auto=format&fit=crop&q=60", fotoAfter: "" }; setWorkOrders(prev => [newWO, ...prev]); sendToGoogleSheets("work_order", newWO); triggerToast(`Temuan kerusakan terdaftar! WO ${newWO.id} dikirim ke Leader.`, "success"); } else { triggerToast(`Inspeksi berhasil! Armada ${noBody} dalam kondisi PRIMA (Siap Jalan).`, "success"); } setTempBase64Image(''); e.target.reset(); }; const handleSelfReportSubmit = (e) => { e.preventDefault(); const formData = new FormData(e.target); const noBody = formData.get('noBody').toUpperCase().trim(); const keluhan = formData.get('keluhan').trim(); const kmMasuk = parseInt(formData.get('kmMasuk') || '0'); const partCode = formData.get('partCode'); const partQty = parseInt(formData.get('partQty') || '0'); if (!noBody || !keluhan) { triggerToast("Mohon isi nomor armada dan detail temuan!", "error"); return; } let partsUsed = []; if (partCode && partQty > 0) { const targetPart = spareparts.find(p => p.kode === partCode); if (targetPart) { if (targetPart.qty < partQty) { triggerToast(`Stok ${targetPart.nama} tidak cukup! (Tersedia: ${targetPart.qty})`, "error"); return; } partsUsed.push({ kode: partCode, nama: targetPart.nama, qty: partQty }); const updatedPart = { ...targetPart, qty: targetPart.qty - partQty }; setSpareparts(prev => prev.map(p => p.kode === partCode ? updatedPart : p)); // Update stok sparepart di Google Sheets sendToGoogleSheets("spareparts", updatedPart); } } const newWO = { id: `WO-M-${Date.now().toString().slice(-4)}`, noBody, kmMasuk, keluhan: `[TEMUAN SPONTAN] ${keluhan}`, prioritas: "Sedang", status: "Menunggu Approval", mekanik: [currentUserName], jamMulai: new Date().toISOString().slice(0, 16), jamSelesai: new Date().toISOString().slice(0, 16), durasi: 30, tindakan: keluhan, sparepart: partsUsed, checker: currentUserName, tanggal: new Date().toISOString().split('T')[0], fotoBefore: tempBase64Image || "", fotoAfter: tempBase64Image || "" }; setWorkOrders(prev => [newWO, ...prev]); sendToGoogleSheets("work_order", newWO); setTempBase64Image(''); triggerToast(`Laporan Mandiri ${newWO.id} dikirim ke Leader! Stok terpotong.`, "success"); e.target.reset(); setMechanicSubTab('tugas'); }; const handleAssignWO = () => { if (assignedMeks.length === 0) { triggerToast("Silakan tunjuk minimal satu mekanik!", "error"); return; } const targetWO = workOrders.find(w => w.id === leaderAssignWO.id); if (targetWO) { const updatedWO = { ...targetWO, status: "Aktif", mekanik: assignedMeks, jamMulai: new Date().toISOString().slice(0, 16), leaderNotes: leaderNotes || "Lakukan perbaikan dan cek sensor terkait." }; setWorkOrders(prev => prev.map(w => w.id === leaderAssignWO.id ? updatedWO : w)); sendToGoogleSheets("work_order", updatedWO); } triggerToast(`WO ${leaderAssignWO.id} ditugaskan ke: ${assignedMeks.join(', ')}`, "success"); setLeaderAssignWO(null); setAssignedMeks([]); setLeaderNotes(''); }; const handleRejectWO = (woId) => { const targetWO = workOrders.find(w => w.id === woId); if (targetWO) { const rejectedWO = { ...targetWO, status: "Rejected" }; setWorkOrders(prev => prev.map(w => w.id === woId ? rejectedWO : w)); sendToGoogleSheets("work_order", rejectedWO); } triggerToast(`Work Order ${woId} ditolak oleh Leader.`, "warning"); }; const handleCompleteRepair = (e, woId) => { e.preventDefault(); const formData = new FormData(e.target); const tindakan = formData.get('tindakan').trim(); const partCode = formData.get('partCode'); const partQty = parseInt(formData.get('partQty') || '0'); if (!tindakan) { triggerToast("Mohon isi deskripsi tindakan perbaikan!", "error"); return; } let partsUsed = []; if (partCode && partQty > 0) { const targetPart = spareparts.find(p => p.kode === partCode); if (targetPart) { if (targetPart.qty < partQty) { triggerToast(`Stok ${targetPart.nama} tidak cukup! (Tersedia: ${targetPart.qty})`, "error"); return; } partsUsed.push({ kode: partCode, nama: targetPart.nama, qty: partQty }); const updatedPart = { ...targetPart, qty: targetPart.qty - partQty }; setSpareparts(prev => prev.map(p => p.kode === partCode ? updatedPart : p)); // Update stok spareparts di Google Sheets sendToGoogleSheets("spareparts", updatedPart); } } const targetWO = workOrders.find(w => w.id === woId); if (targetWO) { const start = targetWO.jamMulai ? new Date(targetWO.jamMulai) : new Date(); const end = new Date(); const diffMin = Math.max(15, Math.round((end - start) / 60000)); const completedWO = { ...targetWO, status: "Menunggu Approval", jamSelesai: end.toISOString().slice(0, 16), durasi: diffMin, tindakan, sparepart: partsUsed, fotoAfter: tempBase64ImageAfter || "https://images.unsplash.com/photo-1563720223185-11003d516935?w=600&auto=format&fit=crop&q=60" }; setWorkOrders(prev => prev.map(w => w.id === woId ? completedWO : w)); sendToGoogleSheets("work_order", completedWO); } setTempBase64ImageAfter(''); triggerToast(`WO ${woId} selesai diperbaiki! Menunggu persetujuan Leader Mekanik.`, "success"); }; const handleApproveCompletion = (woId) => { const targetWO = workOrders.find(w => w.id === woId); if (targetWO) { const approvedWO = { ...targetWO, status: "Approved" }; setWorkOrders(prev => prev.map(w => w.id === woId ? approvedWO : w)); sendToGoogleSheets("work_order", approvedWO); } triggerToast(`WO ${woId} disetujui, bus siap kembali berdinas!`, "success"); }; const handleResetPMInterval = (noBody) => { setVehiclesPM(prev => prev.map(v => { if (v.noBody === noBody) { const nextTarget = Math.ceil((v.currentKM + 1) / 10000) * 10000 + 10000; triggerToast(`PM Armada ${noBody} direset ke target baru: ${nextTarget.toLocaleString()} KM`, "success"); return { ...v, nextPMTarget: nextTarget }; } return v; })); }; const handleAddStoringByLeader = (e) => { e.preventDefault(); const formData = new FormData(e.target); const noBody = formData.get('noBody').toUpperCase().trim(); const lokasi = formData.get('lokasi').trim(); const kendala = formData.get('kendala').trim(); const selectedMeks = assignedMeks; if (!noBody || !lokasi || !kendala || selectedMeks.length === 0) { triggerToast("Mohon lengkapi data dispatch rescue!", "error"); return; } const newStoring = { id: `STR-${Date.now().toString().slice(-4)}`, noBody, lokasi, kendala, tindakan: "", mekanik: selectedMeks, jamMulai: new Date().toISOString().slice(0, 16), jamSelesai: "", status: "OTW", foto: "" }; setStoringList(prev => [newStoring, ...prev]); sendToGoogleSheets("storing", newStoring); setAssignedMeks([]); triggerToast(`Tim Rescue ${selectedMeks.join(', ')} resmi diberangkatkan ke ${lokasi}!`, "success"); e.target.reset(); }; const handleCompleteStoringByMechanic = (e, storingId) => { e.preventDefault(); const formData = new FormData(e.target); const tindakan = formData.get('tindakan').trim(); if (!tindakan) { triggerToast("Harap ketik rincian tindakan emergency yang Anda lakukan!", "error"); return; } const targetStoring = storingList.find(s => s.id === storingId); if (targetStoring) { const completedStoring = { ...targetStoring, tindakan, status: "Selesai", jamSelesai: new Date().toISOString().slice(0, 16), foto: tempBase64Image || "https://images.unsplash.com/photo-1511919884226-fd3cad34687c?w=600&auto=format&fit=crop&q=60" }; setStoringList(prev => prev.map(s => s.id === storingId ? completedStoring : s)); sendToGoogleSheets("storing", completedStoring); } setTempBase64Image(''); triggerToast(`Laporan Rescue ${storingId} berhasil diselesaikan di lokasi!`, "success"); }; const handleAddTireSwap = (e) => { e.preventDefault(); const formData = new FormData(e.target); const noBody = formData.get('noBody').toUpperCase().trim(); const posisi = formData.get('posisi'); const kmBan = parseInt(formData.get('kmBan') || '0'); const barcodeLama = formData.get('barcodeLama').trim(); const barcodeBaru = formData.get('barcodeBaru').trim(); const selectedMeks = assignedMeks; if (!noBody || !posisi || !kmBan || !barcodeLama || !barcodeBaru || selectedMeks.length === 0) { triggerToast("Data penggantian ban tidak lengkap!", "error"); return; } const tireCode = "SP-TIRE-295"; const tirePart = spareparts.find(p => p.kode === tireCode); if (tirePart && tirePart.qty <= 0) { triggerToast("Gagal! Stok Ban Radial Tubeless di gudang saat ini habis.", "error"); return; } const updatedTire = { ...tirePart, qty: Math.max(0, tirePart.qty - 1) }; setSpareparts(prev => prev.map(p => { if (p.kode === tireCode) { return updatedTire; } return p; })); // Sinkronisasi sisa ban radial ke Google Sheets sendToGoogleSheets("spareparts", updatedTire); const newSwap = { id: `BAN-${Date.now().toString().slice(-4)}`, noBody, posisi, kmBan, barcodeLama, barcodeBaru, mekanik: selectedMeks, jamMulai: new Date().toISOString().slice(0, 16), jamSelesai: new Date().toISOString().slice(0, 16), fotoLama: tempBase64Image || "https://images.unsplash.com/photo-1578844251758-2f71da64c96f?w=400", fotoBaru: tempBase64ImageAfter || "https://images.unsplash.com/photo-1580273916550-e323be2ae537?w=400" }; setTireSwaps(prev => [newSwap, ...prev]); sendToGoogleSheets("ganti_ban", newSwap); setTempBase64Image(''); setTempBase64ImageAfter(''); setAssignedMeks([]); triggerToast(`Data ganti ban berhasil disimpan. Stok Ban Radial gudang terpotong 1 Pcs.`, "success"); e.target.reset(); }; const handleSavePart = (e) => { e.preventDefault(); if (!newPartForm.kode || !newPartForm.nama || newPartForm.qty === '') { triggerToast("Kode, Nama, dan Kuantitas Suku Cadang wajib diisi!", "error"); return; } const codeUpper = newPartForm.kode.toUpperCase().trim(); const qtyInt = parseInt(newPartForm.qty); const minQtyInt = parseInt(newPartForm.minQty || '3'); const existsIndex = spareparts.findIndex(p => p.kode === codeUpper); const updatedPart = { kode: codeUpper, nama: newPartForm.nama, qty: qtyInt, unit: newPartForm.unit, minQty: minQtyInt }; if (existsIndex >= 0 && isEditingPart !== 'new') { setSpareparts(prev => prev.map((p, idx) => idx === existsIndex ? updatedPart : p)); triggerToast(`Data Suku Cadang ${codeUpper} diperbarui!`, "success"); // Kirim pembaruan suku cadang ke Google Sheets (Update Baris) sendToGoogleSheets("spareparts", updatedPart); } else { if (spareparts.find(p => p.kode === codeUpper)) { triggerToast("Kode suku cadang sudah digunakan!", "error"); return; } setSpareparts(prev => [...prev, updatedPart]); triggerToast(`Suku cadang baru ${codeUpper} didaftarkan!`, "success"); // Kirim suku cadang baru ke Google Sheets (Tambah Baris) sendToGoogleSheets("spareparts", updatedPart); } setNewPartForm({ kode: '', nama: '', qty: '', unit: 'Pcs', minQty: '3' }); setIsEditingPart(null); setShowAddPartModal(false); }; const handleRemovePart = (code) => { setSpareparts(prev => prev.filter(p => p.kode !== code)); triggerToast(`Suku cadang ${code} dihapus.`, "warning"); }; const triggerResetSystem = () => { setWorkOrders(DEFAULT_WORK_ORDERS); setSpareparts(DEFAULT_SPAREPARTS); setVehiclesPM(DEFAULT_VEHICLES_PM); setStoringList(DEFAULT_STORING); setTireSwaps(DEFAULT_TIRE_SWAPS); setInspectionLogs([]); triggerToast("Sistem bengkel berhasil di-reset ke setelan awal.", "warning"); setShowConfirmReset(false); }; const filteredWorkOrders = useMemo(() => { return workOrders.filter(w => { const matchText = `${w.id} ${w.noBody} ${w.keluhan} ${w.prioritas} ${w.status} ${w.checker}`.toLowerCase(); return matchText.includes(searchQuery.toLowerCase()); }); }, [workOrders, searchQuery]); const myMechanicJobs = useMemo(() => { return workOrders.filter(w => w.mekanik.includes(currentUserName) && w.status === 'Aktif'); }, [workOrders, currentUserName]); const myMechanicStoringJobs = useMemo(() => { return storingList.filter(s => s.mekanik.includes(currentUserName) && s.status === 'OTW'); }, [storingList, currentUserName]); const handleExportCSV = (type) => { let headers = []; let rows = []; let filename = `laporan_${type}_${Date.now()}.csv`; const cleanCell = (val) => { if (val === undefined || val === null) return '""'; let str = String(val); str = str.replace(/\r?\n|\r/g, ' '); str = str.replace(/"/g, '""'); return `"${str}"`; }; if (type === 'work_orders') { headers = ['ID WO', 'No Body', 'KM Masuk', 'Keluhan', 'Prioritas', 'Status', 'Mekanik', 'Tanggal', 'Tindakan', 'Checker']; rows = workOrders.map(w => [ cleanCell(w.id), cleanCell(w.noBody), cleanCell(w.kmMasuk), cleanCell(w.keluhan), cleanCell(w.prioritas), cleanCell(w.status), cleanCell(w.mekanik.join(', ')), cleanCell(w.tanggal), cleanCell(w.tindakan || 'Belum ada'), cleanCell(w.checker) ]); } else { headers = ['Kode', 'Nama Suku Cadang', 'Jumlah Stok', 'Satuan', 'Batas Minimum']; rows = spareparts.map(p => [ cleanCell(p.kode), cleanCell(p.nama), cleanCell(p.qty), cleanCell(p.unit), cleanCell(p.minQty) ]); } const delimiter = ";"; const csvContent = "\uFEFF" + `sep=${delimiter}\n` + headers.join(delimiter) + "\n" + rows.map(r => r.join(delimiter)).join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.setAttribute("href", url); link.setAttribute("download", filename); document.body.appendChild(link); link.click(); document.body.removeChild(link); triggerToast(`Ekspor ${filename} sukses! Format rapi & terbagi kolom Excel.`, "success"); }; if (!isLoggedIn) { return (
{toasts.map(t => (
{t.type === 'success' && } {t.type === 'warning' && } {t.type === 'error' && }

{t.msg}

))}

BENGKEL TRANSJAKARTA

Sistem Perawatan Bus & Integrasi Depo

); } return (
{/* 2. DATALIST UNTUK AUTOCOMPLETE NOMOR BODY BUS */} {FLEET_UNITS.map(unit => (
{toasts.map(t => (
{t.type === 'success' && } {t.type === 'warning' && } {t.type === 'error' && }

{t.msg}

))}

BENGKEL TRANSJAKARTA RAWA BUAYA

Transjakarta Maintenance Management System

{currentUserName} {currentRole}
{/* ================================================== */} {/* TAB: ANALITIK UTAMA (DASHBOARD) */} {/* ================================================== */} {activeTab === 'dashboard' && (currentRole === 'admin' || currentRole === 'leader') && (

Analitik & Dashboard Pemantauan Depo

Status real-time armada bus, ketersediaan suku cadang, dan performa mekanik.

{/* KPI Cards */}
Antrean WO Pending
{stats.pending} Butuh Tindakan
Sedang Dikerjakan
{stats.aktif} Mekanik Tiket
Overdue PM 10K
{stats.overduePM} Armada Alert
Stok Suku Cadang Limit
{stats.lowStockCount} Kritis
{/* Main Dashboard Rows */}
{/* Mechanic Leaderboard */}

Papan Performa Mekanik Shift Ini

{stats.performance.slice(0, 7).map((p, idx) => ( ))}
Nama Mekanik Tugas Aktif WO Sukses Storing/Rescue Ganti Ban Skor Kerja
#{idx+1} {p.name} {p.assignedCount} {p.completedCount} {p.storingCount} {p.tireCount} {p.score}
{/* Overdue/Near PM List */}

Peringatan Servis Berkala Mendesak

{vehiclesPM.filter(v => v.currentKM >= v.nextPMTarget || (v.nextPMTarget - v.currentKM) <= 2000).map(v => { const sisa = v.nextPMTarget - v.currentKM; const isOverdue = sisa <= 0; return (
Armada {v.noBody} • {v.model}
Odo: {v.currentKM.toLocaleString()} / Target: {v.nextPMTarget.toLocaleString()} KM
{isOverdue ? `Lewat ${Math.abs(sisa).toLocaleString()} KM` : `Sisa ${sisa.toLocaleString()} KM`} STATUS SERVIS
); })} {vehiclesPM.filter(v => v.currentKM >= v.nextPMTarget || (v.nextPMTarget - v.currentKM) <= 2000).length === 0 && (

Semua unit bus dalam siklus preventive maintenance yang aman.

)}
{/* Visual WO Distribution */}

Distribusi Status Work Order

Antrean Menunggu Tindakan (Pending) {stats.pending} WO
0 ? (stats.pending / stats.total) * 100 : 0}%` }}>
Sedang Dikerjakan Mekanik (Aktif) {stats.aktif} WO
0 ? (stats.aktif / stats.total) * 100 : 0}%` }}>
Menunggu Approval Selesai Kerja {stats.waiting} WO
0 ? (stats.waiting / stats.total) * 100 : 0}%` }}>
Pekerjaan Selesai & Disetujui (Approved) {stats.approved} WO
0 ? (stats.approved / stats.total) * 100 : 0}%` }}>
{/* Suku Cadang Kritis */}

Suku Cadang di Bawah Batas Minimum

{stats.lowStockParts.map(p => (
{p.nama} {p.kode}
{p.qty} sisa Safety Limit: {p.minQty}
))} {stats.lowStockCount === 0 && (

Ketersediaan logistik suku cadang aman terkendali.

)}
)} {/* ================================================== */} {/* TAB: INPUT INSPEKSI BARU (CHECKER ONLY) */} {/* ================================================== */} {activeTab === 'checker_form' && currentRole === 'checker' && (

Formulir Inspeksi Harian Armada

Pemeriksa: {currentUserName} • Catat kondisi aktual dan odometer bus sebelum beroperasi.

{tempBase64Image && (
Bukti
)}
)} {/* ================================================== */} {/* TAB: LOG INSPEKSI SAYA (CHECKER ONLY) */} {/* ================================================== */} {activeTab === 'checker_history' && currentRole === 'checker' && (

Log Hasil Inspeksi Saya

Daftar riwayat pemeriksaan armada bus yang telah Anda input.

{inspectionLogs.length === 0 ? (

Belum ada riwayat inspeksi yang dicatat hari ini.

) : ( inspectionLogs.filter(log => log.checker === currentUserName).map(log => (
{log.id} Armada {log.noBody}
{log.status}
Odometer / KM:

{log.kmMasuk.toLocaleString()} KM

Temuan Kerusakan:

“{log.keluhan}”

Skala Prioritas:

{log.prioritas}

{log.fotoBefore && (
Foto Bukti Fisik: Visual setZoomImgUrl(log.fotoBefore)} className="w-full h-24 object-cover rounded-xl border border-slate-800 cursor-zoom-in hover:opacity-90" />
)}
)) )}
)} {/* ================================================== */} {/* TAB: PEKERJAAN AKTIF MEKANIK (MECHANIC ONLY) */} {/* ================================================== */} {activeTab === 'mechanic_tasks' && currentRole === 'mechanic' && (

Workspace Perbaikan Mekanik: {currentUserName}

Shift Aktif Anda. Selesaikan tugas bengkel & emergency storing di bawah ini.

{/* NAVIGASI SUB-TAB MEKANIK */}
{mechanicSubTab === 'tugas' ? ( <> {/* EMERGENCY ROAD RESCUE UNTUK MEKANIK */} {myMechanicStoringJobs.length > 0 && (

🚨 TUGAS EMERGENCY ROAD SERVICE (STORING JALAN)

{myMechanicStoringJobs.map(st => (
DITUGASKAN - OTW TKP

{st.id} • Bus {st.noBody}

Waktu Keluar: {st.jamMulai.replace('T', ' ')}
LOKASI DARURAT: {st.lokasi}
KENDALA AWAL DILAPORKAN PRAMUDI: “{st.kendala}”
handleCompleteStoringByMechanic(e, st.id)} className="bg-slate-955/60 p-4 rounded-xl border border-slate-850 space-y-3.5">
Laporan Penyelesaian TKP (Isi Saat Masalah Teratasi)
{tempBase64Image && ( Preview Storing )}
))}
)} {/* TUGAS WORKSHOP BIASA MEKANIK */}

TUGAS PERBAIKAN DI BAY BENGKEL

{myMechanicJobs.map(wo => (
Identitas Tiket

{wo.id} • Body {wo.noBody}

KM Masuk: {wo.kmMasuk.toLocaleString()} KM

Aduan / Temuan

“{wo.keluhan}”

{wo.fotoBefore && (
Visual Damage Damage setZoomImgUrl(wo.fotoBefore)} className="w-full h-32 object-cover rounded-xl border border-slate-800 cursor-zoom-in hover:opacity-90" />
)}
Instruksi Leader

{wo.leaderNotes || "Lakukan penelusuran menyeluruh."}

Status Suku Cadang Gunakan form di sebelah kanan jika memerlukan penggantian bagian.

Form Selesai Kerja

handleCompleteRepair(e, wo.id)} className="space-y-3">
{tempBase64ImageAfter && ( Preview After )}
))} {myMechanicJobs.length === 0 && myMechanicStoringJobs.length === 0 && (

Tidak ada pekerjaan aktif yang ditugaskan kepada Anda saat ini.

)}
) : (

Catat Temuan Kerusakan Spontan

Formulir laporan ini akan menghasilkan WO berawalan WO-M-xxxx and otomatis memotong stok gudang jika ada komponen yang digunakan.

Pemakaian Suku Cadang Gudang

{tempBase64Image && (
Bukti
)}
)}
)} {/* ================================================== */} {/* TAB: SEMUA WORK ORDERS */} {/* ================================================== */} {activeTab === 'workorders' && currentRole !== 'checker' && (

Semua Histori & Antrean Work Orders (WO)

Review dan monitoring seluruh aduan yang masuk ke dalam sistem.

setSearchQuery(e.target.value)} className="w-full bg-slate-900 border border-slate-800 rounded-xl pl-9 pr-4 py-2.5 text-xs text-white focus:outline-none focus:border-cyan-500 font-semibold" />
{filteredWorkOrders.map(wo => (
{wo.id} Body: {wo.noBody} | KM: {wo.kmMasuk.toLocaleString()}
Prioritas {wo.prioritas} {wo.status}
Aduan Masuk

“{wo.keluhan}”

Checker: {wo.checker} • {wo.tanggal}
{wo.fotoBefore && (
Foto Kerusakan (Before) Before setZoomImgUrl(wo.fotoBefore)} className="w-full h-24 object-cover rounded-xl border border-slate-800 cursor-zoom-in hover:opacity-90" />
)}
Tim Pelaksana

Mekanik: {wo.mekanik.join(', ') || 'Belum Ditunjuk'}

{wo.durasi > 0 &&

Durasi Kerja: {wo.durasi} Menit

} {wo.leaderNotes &&

Instruksi: {wo.leaderNotes}

}
{wo.tindakan && (
Tindakan Teknis Mekanik:

{wo.tindakan}

)}
Suku Cadang Digunakan {wo.sparepart.length > 0 ? (
{wo.sparepart.map(p => (
{p.nama} x{p.qty}
))}
) : (

Tanpa suku cadang tambahan.

)} {wo.fotoAfter && (
Foto Hasil Kerja (After) After setZoomImgUrl(wo.fotoAfter)} className="w-full h-24 object-cover rounded-xl border border-slate-800 cursor-zoom-in hover:opacity-90" />
)} {currentRole === 'leader' && wo.status === 'Pending' && (
)} {currentRole === 'leader' && wo.status === 'Menunggu Approval' && ( )}
))}
)} {/* ================================================== */} {/* TAB: MONITORING PM BERKALA 10.000 KM */} {/* ================================================== */} {activeTab === 'preventive' && currentRole !== 'checker' && (

Preventive Maintenance (PM) Berkala

Sistem alert otomatis servis berkala setiap interval 10.000 Kilometer armada.

{vehiclesPM.map(v => { const sisaKM = v.nextPMTarget - v.currentKM; const ratio = Math.max(0, Math.min(100, ((10000 - sisaKM) / 10000) * 100)); let barColor = "from-emerald-500 to-teal-500"; let borderClass = "border-slate-800 bg-slate-900/60"; let badgeText = "Aman / Safe"; let badgeColor = "bg-emerald-500/10 text-emerald-400 border-emerald-500/20"; if (sisaKM <= 0) { barColor = "from-red-500 to-rose-600 animate-pulse"; borderClass = "border-red-500/30 bg-red-955/10"; badgeText = "Overdue Servis"; badgeColor = "bg-red-500/20 text-red-400 border-red-500/30 animate-pulse"; } else if (sisaKM <= 2000) { barColor = "from-amber-500 to-yellow-500"; borderClass = "border-amber-500/25 bg-amber-955/10"; badgeText = "Mendekati PM"; badgeColor = "bg-amber-500/10 text-amber-400 border-amber-500/20"; } return (
{v.model}

TJ Body {v.noBody}

{badgeText}
Km Sekarang {v.currentKM.toLocaleString()} KM
Target PM {v.nextPMTarget.toLocaleString()} KM
Sisa Jarak Tempuh {sisaKM > 0 ? `${sisaKM.toLocaleString()} KM` : `${Math.abs(sisaKM).toLocaleString()} KM OVERDUE`}
{(currentRole === 'leader' || currentRole === 'admin') && ( )}
); })}
)} {/* ================================================== */} {/* TAB: STORING JALAN (RESCUE EMERGENCY) */} {/* ================================================== */} {activeTab === 'storing' && currentRole !== 'checker' && (

Emergency Road Service (Storing Rescue)

Penanganan kerusakan darurat armada di luar area depo secara real-time.

{/* FORM DISPATCH OLEH LEADER */} {(currentRole === 'leader' || currentRole === 'admin') ? (

Dispatch Rescue Baru

{MECHANICS_LIST.map(name => { const isSelected = assignedMeks.includes(name); return ( ); })}
) : (
Modul input penugasan dispatch storing jalan hanya diizinkan untuk Leader dan Admin.
)} {/* HISTORI STORING JALAN */}

Histori Operasi Penyelamatan Jalan

{storingList.map(s => (
{s.id} • TJ Body {s.noBody} {s.status === 'Selesai' ? 'RESCUE SUKSES' : 'DALAM DISPATCH OTW'}
LOKASI BREAKDOWN:

{s.lokasi}

KENDALA AWAL DILAPORKAN:

“{s.kendala}”

TINDAKAN RECOVERY (OLEH MEKANIK TKP): {s.tindakan ? (

{s.tindakan}

) : (

Mekanik masih dalam perjalanan menuju lokasi...

)} MEKANIK PENYELAMAT:

{s.mekanik.join(', ')}

{s.foto && (
Bukti Foto Lokasi Kejadian: TKP setZoomImgUrl(s.foto)} className="w-full h-32 object-cover rounded-xl border border-slate-855 cursor-zoom-in hover:opacity-90" />
)}
))}
)} {/* ================================================== */} {/* TAB: PENGGANTIAN BAN ARMADA */} {/* ================================================== */} {activeTab === 'tires' && currentRole !== 'checker' && (

Log Penggantian Ban Bus Depo

Pendaftaran penggantian roda bus akan otomatis memotong 1 Pcs stok ban radial di gudang.

Penggantian Roda Ban Baru

{tempBase64Image && ( Ban )}
{MECHANICS_LIST.map(name => { const isSelected = assignedMeks.includes(name); return ( ); })}

Histori Log Ban Depo

{tireSwaps.map(t => (
{t.id} Armada {t.noBody}

Posisi: {t.posisi}

Masa Pakai Odo Ban: {t.kmBan.toLocaleString()} KM

Old: {t.barcodeLama} New: {t.barcodeBaru}
Swapper Mekanik: {t.mekanik.join(', ')} {t.fotoLama && ( Barcode setZoomImgUrl(t.fotoLama)} className="w-8 h-8 object-cover rounded border border-slate-800 mt-2 ml-auto cursor-zoom-in hover:opacity-90" /> )}
))}
)} {/* ================================================== */} {/* TAB: GUDANG SPAREPART INVENTORY */} {/* ================================================== */} {activeTab === 'gudang' && currentRole !== 'checker' && (

Logistik & Gudang Suku Cadang

Kelola inventaris suku cadang, update minimum safety stock, restock instan.

{(currentRole === 'admin' || currentRole === 'leader') && ( )}
{spareparts.map(p => { const isLimit = p.qty <= p.minQty; return (
{p.kode} {isLimit && ( Stok Kritis )}

{p.nama}

Minimum Safety: {p.minQty} {p.unit}

{p.qty} {p.unit}
{(currentRole === 'admin' || currentRole === 'leader') && (
{currentRole === 'admin' && ( )}
)}
); })}
)} {/* ================================================== */} {/* TAB: SPREADSHEET SETTINGS & WEBHOOK */} {/* ================================================== */} {activeTab === 'google_sheets' && (currentRole === 'admin' || currentRole === 'leader') && (

Integrasi Sinkronisasi Google Sheets

Hubungkan database bengkel lokal Anda langsung dengan Google Spreadsheet milik pribadi Anda demi pencadangan data tanpa batas secara aman dan otomatis.

setSpreadsheetUrl(e.target.value)} className="flex-1 bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-xs text-white focus:outline-none focus:border-emerald-500 font-mono" /> {spreadsheetUrl && ( )}

Apabila diisi, data penambahan inspeksi, log WO, rescue storing, dan ganti ban akan otomatis meluncur ke spreadsheet secara *real-time*.

Panduan Pemasangan API Google Sheets (Hanya 3 Menit):

  1. Buat Spreadsheet Baru di Google Drive Anda.
  2. Di bilah menu atas Google Sheets, klik menu **Ekstensi** lalu pilih **Apps Script**.
  3. Hapus semua baris kode bawaan di dalam editor tersebut, lalu **Salin & Tempel** seluruh blok kode JavaScript di bawah ini:
{/* CODE SNIPPET AREA FOR THE USER */}
GoogleAppsScript.gs
{`function doPost(e) {
  try {
    var content = JSON.parse(e.postData.contents);
    var sheetType = content.sheet;
    var dataObj = content.data;
    
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    var sheet = ss.getSheetByName(sheetType);
    
    if (!sheet) { sheet = ss.insertSheet(sheetType); }
    
    var headers = Object.keys(dataObj);
    if (sheet.getLastRow() === 0) {
      sheet.appendRow(headers);
    } else {
      headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
    }
    
    var rowValues = headers.map(function(key) {
      var val = dataObj[key] !== undefined ? dataObj[key] : "";
      return typeof val === 'object' ? JSON.stringify(val) : val;
    });
    
    var idToFind = dataObj.id || dataObj.id_wo || dataObj.kode;
    var foundRow = -1;
    
    if (idToFind && sheet.getLastRow() > 1) {
       var data = sheet.getDataRange().getValues();
       var idColIndex = headers.indexOf('id');
       if (idColIndex === -1) { idColIndex = headers.indexOf('id_wo'); }
       if (idColIndex === -1) { idColIndex = headers.indexOf('kode'); }
       
       if (idColIndex !== -1) {
         for (var i = 1; i < data.length; i++) {
           if (String(data[i][idColIndex]).trim() === String(idToFind).trim()) {
             foundRow = i + 1; break;
           }
         }
       }
    }
    
    if (foundRow !== -1) {
      sheet.getRange(foundRow, 1, 1, rowValues.length).setValues([rowValues]);
    } else {
      sheet.appendRow(rowValues);
    }
    
    return ContentService.createTextOutput(JSON.stringify({ status: "success", message: foundRow !== -1 ? "Updated row " + foundRow : "Appended new row" })).setMimeType(ContentService.MimeType.JSON);
  } catch (err) {
    return ContentService.createTextOutput(JSON.stringify({ status: "error", error: err.toString() })).setMimeType(ContentService.MimeType.JSON);
  }
}`}
                    
  1. Klik ikon disket (Simpan) di Google Apps Script.
  2. Klik tombol biru **Terapkan** (*Deploy*) > pilih **Penerapan Baru** (*New Deployment*).
  3. Pilih jenis terapkan: **Aplikasi Web** (*Web App*).
  4. Atur aksesibilitas bidang "Yang memiliki akses" (*Who has access*) menjadi **Siapa Saja** (*Anyone*). Ini wajib agar aplikasi React lokal Anda diizinkan mengirim baris data.
  5. Klik **Terapkan**, setujui izin verifikasi Google Akun Anda, lalu **Salin URL Aplikasi Web** yang tampil di layar, kemudian tempelkan di kotak isian input di atas!
)}
{/* ================================================== */} {/* INTERACTIVE MODALS & OVERLAYS */} {/* ================================================== */} {/* MODAL ASSIGN MEKANIK (LEADER) */} {leaderAssignWO && (
Otorisasi Leader

Penugasan & Persetujuan WO: {leaderAssignWO.id}

Bus Body: {leaderAssignWO.noBody} • Referensi Aduan: “{leaderAssignWO.keluhan}”

{MECHANICS_LIST.map(name => { const isSelected = assignedMeks.includes(name); return ( ); })}
setLeaderNotes(e.target.value)} placeholder="Contoh: Bongkar kaliper rem..." className="w-full bg-slate-950 border border-slate-800 rounded-xl p-2 text-white focus:outline-none" />
)} {/* MODAL REGISTER/EDIT SPAREPART */} {showAddPartModal && (

{isEditingPart === 'new' ? 'Daftarkan Sparepart Baru' : `Edit Suku Cadang: ${newPartForm.kode}`}

setNewPartForm({...newPartForm, kode: e.target.value})} placeholder="Contoh: SP-ALT-24V" className="w-full bg-slate-950 border border-slate-800 rounded-xl p-2 text-white focus:outline-none font-mono" required disabled={isEditingPart !== 'new'} />
setNewPartForm({...newPartForm, nama: e.target.value})} placeholder="Contoh: Alternator Heavy Duty" className="w-full bg-slate-950 border border-slate-800 rounded-xl p-2 text-white focus:outline-none" required />
setNewPartForm({...newPartForm, qty: e.target.value})} type="number" placeholder="Qty" className="w-full bg-slate-950 border border-slate-800 rounded-xl p-2 text-white focus:outline-none font-bold text-cyan-400" required />
setNewPartForm({...newPartForm, unit: e.target.value})} placeholder="Pcs / Set" className="w-full bg-slate-950 border border-slate-800 rounded-xl p-2 text-white" required />
setNewPartForm({...newPartForm, minQty: e.target.value})} type="number" placeholder="Min" className="w-full bg-slate-950 border border-slate-800 rounded-xl p-2 text-white" required />
)} {/* MODAL RESET SYSTEM CONFIRMATION */} {showConfirmReset && (

Konfirmasi Reset Data

Seluruh log perbaikan, data pergantian ban, storing, dan stok gudang akan dikembalikan ke setelan default pabrikan.

)} {/* MODAL IMAGE ZOOM VIEW */} {zoomImgUrl && (
setZoomImgUrl(null)}>
Visual Detail
)}
); } function LoginForm({ onSubmitLogin }) { const [selectedRole, setSelectedRole] = useState('checker'); const [selectedName, setSelectedName] = useState(''); useEffect(() => { if (selectedRole === 'mechanic') setSelectedName(MECHANICS_LIST[0]); else if (selectedRole === 'checker') setSelectedName(CHECKERS_LIST[0]); else if (selectedRole === 'leader') setSelectedName("Alvi (Leader Shift A)"); else setSelectedName("Administrator Utama"); }, [selectedRole]); return (
{ e.preventDefault(); onSubmitLogin(selectedRole, selectedName); }} className="space-y-4 text-xs" >
{selectedRole === 'mechanic' && ( )} {selectedRole === 'checker' && ( )} {selectedRole === 'leader' && ( )} {selectedRole === 'admin' && ( )}
); }